package util.core;

import util.exceptions.UnknownElementException;
import util.input.InputFixer;
import util.input.Parser;

import java.util.ArrayList;

public class Molecule {
    private ArrayList<MoleculeComponent> parts;
    private String name;
    private int charge;

    /**
     * Creates a new Molecule
     *
     * @param c The formula which represents the molecule
     * @throws UnknownElementException if molecule does not exist
     */
    public Molecule(String c) throws UnknownElementException {
        parts = new ArrayList<MoleculeComponent>();
        if (c.equals("e")) {
            name = "";
            charge = -1;
        } else if (c.equals("p")) {
            name = "H";
            charge = 1;
            parts.add(new MoleculeComponent(name, 1));
        } else {
            c = removeCharge(c);
            InputFixer fixer = new InputFixer(c);
            fixer.fix1();
            c = fixer.getResult();
            name = c;
            c = removeHydrate(c);
            c = Parser.removeSurrounding(c, "()");
            c = Parser.removeSurrounding(c, "[]");

            int startIndex;
            int endIndex = 0;
            while (endIndex < c.length()) {
                String element;
                int multiplier = 0;
                startIndex = endIndex;
                element = getNextElement(c, startIndex);
                endIndex += element.length();
                if (endIndex == c.length()) {
                    multiplier = 1;
                }
                if (multiplier == 0) {
                    startIndex = endIndex;
                    String result = getNextNumber(c, startIndex);
                    endIndex += result.length();
                    if (result.length() == 0)
                        multiplier = 1;
                    else
                        multiplier = Integer.parseInt(result);
                }

                parts.add(new MoleculeComponent(element, multiplier));

            }
            checkForMultiples();
        }
    }


    private String getNextElement(String c, int startIndex) {
        int endIndex;
        endIndex = Parser.findEndName(c, startIndex);
        if (startIndex == endIndex)
            return "";
        else
            return c.substring(startIndex, endIndex);
    }

    private String getNextNumber(String c, int startIndex) {
        int endIndex;
        endIndex = Parser.findEndNum(c, startIndex);
        return c.substring(startIndex, endIndex);
    }

    /**
     * Checks for whether there are multiples of the same Atom. For example, COOH would have multiples of O
     *
     * @throws util.exceptions.UnknownElementException
     *          s
     */
    private void checkForMultiples() throws UnknownElementException {
        ArrayList<Atom> atoms = getAtoms();
        for (Atom m : atoms) {
            int count = countOccurences(m);
            if (count > 1) {
                int totalMultiplicity = 0;
                for (int i = 0; i < parts.size(); i++) {
                    if (parts.get(i).getElement().equals(m)) {
                        totalMultiplicity += parts.get(i).getMultiplicity();
                        parts.remove(i);
                        i--;
                    }
                }
                parts.add(new MoleculeComponent(m.getSymbol(), totalMultiplicity));
            }
        }
    }

    private int countOccurences(Atom m) {
        int count = 0;
        for (MoleculeComponent c : parts) {
            if (c.getElement().equals(m)) {
                count++;
            }
        }
        return count;
    }


    private String removeCharge(String c) {
        if (c.contains("{")) {
            int left = c.indexOf("{");
            int right = c.indexOf("}");
            String charge;
            if (left != -1) {
                charge = c.substring(left + 1, right);
                int multiplier = 1;
                if (charge.contains("+")) {
                    charge = charge.replace("+", "");
                } else if (charge.contains("-")) {
                    charge = charge.replace("-", "");
                    multiplier = -1;
                }
                if (charge.length() != 0)
                    this.charge = Integer.parseInt(charge.replaceAll(" ", "")) * multiplier;
                else {
                    this.charge = multiplier;
                }
                return c.replace(c.substring(left), "");
            }
        } else if (c.contains("+")) {
            int index = c.indexOf("+");
            if (index < c.length() - 1 && Parser.isNumber(c.charAt(index + 1))) {
                return removeImplicitChargeValue(index, 1, c);
            }
            String stringPlus = c.replaceAll("\\+*", "");
            charge = c.length() - stringPlus.length() - 1;
            return stringPlus;
        } else if (c.contains("-")) {
            int index = c.indexOf("-");
            if (index < c.length() - 1 && Parser.isNumber(c.charAt(index + 1))) {
                return removeImplicitChargeValue(index, -1, c);
            }
            String stringMinus = c.replaceAll("\\-*", "");
            charge = stringMinus.length() - c.length();
            return stringMinus;
        }
        return c;
    }

    private String removeImplicitChargeValue(int index, int multiplier, String c) {
        int end = Parser.findEndNum(c, index + 1);
        charge = multiplier * Integer.parseInt(c.substring(index + 1, end));
        return c.substring(0, index);
    }

    public String removeHydrate(String c) {
        if (c.contains("*")) {
            int pos = c.indexOf("*");
            String hydrate = c.substring(pos + 1);
            int end = Parser.findEndNum(hydrate, 0);
            if (end != 0)
                return c.substring(0, pos) + "(" + hydrate.substring(end, end + 3) + ")" + Integer.parseInt(hydrate.substring(0, end)) + hydrate.substring(end + 3);
            else
                return c.replace("*", "");
        }
        return c;
    }


    /**
     * Gets the elements within the Molecule
     *
     * @return An array of MoleculeComponents
     */
    public ArrayList<MoleculeComponent> getElements() {
        return parts;
    }


    /**
     * Whether or not the atom contains the specified component
     *
     * @param a The component to be found
     * @return true if it is contained, false otherwise
     */
    public boolean contains(MoleculeComponent a) {
        for (MoleculeComponent j : parts) {
            if (j.equals(a))
                return true;
        }
        return false;
    }

    public boolean contains(Atom a) {
        for (MoleculeComponent j : parts) {
            if (j.getElement().equals(a))
                return true;
        }
        return false;
    }

    /**
     * Gets the amount of atoms specified
     *
     * @param a The atom specified
     * @return Returns the multiplicity if it is contained, 0 otherwise
     */
    public int getCount(Atom a) {
        for (MoleculeComponent j : parts) {
            if (j.getElement().equals(a))
                return j.getMultiplicity();
        }
        return 0;
    }

    /**
     * Gets the atoms which are contained in the equation
     *
     * @return An ArrayList of the atoms
     */
    private ArrayList<Atom> getAtoms() {
        ArrayList<Atom> atoms = new ArrayList<Atom>();
        for (MoleculeComponent g : parts) {
            if (!atoms.contains(g.getElement())) {
                atoms.add(g.getElement());
            }
        }
        return atoms;
    }

    public boolean isSingleElement() {
        return parts.size() == 1;
    }

    @Override
    public boolean equals(Object o) {
        Molecule other;
        if (o instanceof Molecule)
            other = (Molecule) o;
        else
            return false;
        if (other.getElements().size() != parts.size() || charge != other.getCharge())
            return false;

        for (MoleculeComponent m : other.getElements()) {
            if (!contains(m)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public String toString() {
        if (!name.equals(""))
            return name;
        else if (charge == -1)
            return "e";
        else if (charge == 1)
            return "p";
        return null;
    }


    public int getCharge() {
        return charge;
    }

    public String getName() {
        return name;
    }

}
